/*Scala code license:

 Copyright 2009/10 Osman Pub

 Licensed under the Apache License, Version 2.0 (the "License") you may not use
 this file except in compliance with the License.

 You may obtain a copy of the License at

 http://www.apache.org/licenses/LICENSE-2.0

 Unless required by applicable law or agreed to in writing, software distributed
 under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
 CONDITIONS OF ANY KIND, either express or implied. See the License for the
 specific language governing permissions and limitations under the License.

 */
/**
 Orginal java code credits:
 * @author Jeff Kirby
 */
package demos

import common._
import java.awt.event._
import javax.media.opengl._
import javax.media.opengl.glu._
import javax.swing._

object Lesson19 {
  def main(args: Array[String]) {
    val glDisplay = GLDisplay("Lesson 19: Particle engine using triangle strips")
    glDisplay addGLEventListener(new Renderer)
    glDisplay addKeyListener(new KeyAdapter {
      override def keyPressed(e: KeyEvent) =
        response = e getKeyCode match {
          case KeyEvent.VK_ADD =>Action speedUp
          case KeyEvent.VK_SUBTRACT => Action slowDown
          case KeyEvent.VK_NUMPAD8 => Action pullUp
          case KeyEvent.VK_NUMPAD2 => Action pullDown
          case KeyEvent.VK_NUMPAD4 => Action pullLeft
          case KeyEvent.VK_NUMPAD6 => Action pullRight
          case KeyEvent.VK_UP => Action incX
          case KeyEvent.VK_DOWN => Action decX
          case KeyEvent.VK_RIGHT => Action incY
          case KeyEvent.VK_LEFT => Action decY
          case KeyEvent.VK_PAGE_UP => Action zoomIn
          case KeyEvent.VK_PAGE_DOWN => Action zoomOut
          case _ => Action none
        }

      override def keyReleased(e: KeyEvent) =
        e getKeyCode match {
          case KeyEvent.VK_ESCAPE => glDisplay stop
          case KeyEvent.VK_R => response = Action rainbow
          case KeyEvent.VK_SPACE => response = Action color
          case KeyEvent.VK_B => response = Action burst
          case _ => response = Action none
        }
    })
    glDisplay start
  }

  object Action extends Enumeration { val burst, color, incX, decX, incY, decY,
    none, pullDown, pullLeft, pullRight, pullUp, rainbow, slowDown, speedUp,
    zoomIn, zoomOut = Value }
  private var response = Action none

  private val MAX_PARTICLES = 1000
  private class Particle(var active: Boolean = false, var life: Float = 0,
                         var fade: Float = 0, var r: Float = 0, var g: Float = 0, var b: Float = 0,
                         var x: Float = 0, var y: Float = 0, var z: Float = 0, var xi: Float = 0,
                         var yi: Float = 0, var zi: Float = 0, var xg: Float = 0, var yg: Float = 0,
                         var zg: Float = 0) {
    /*boolean active;					// Active (Yes/No)
     float life;					// Particle Life
     float fade;					// Fade Speed
     float r;						// Red Value
     float g;						// Green Value
     float b;						// Blue Value
     float x;						// X Position
     float y;						// Y Position
     float z;						// Z Position
     float xi;						// X Direction
     float yi;						// Y Direction
     float zi;						// Z Direction
     float xg;						// X Gravity
     float yg;						// Y Gravity
     float zg;						// Z Gravity*/
  }

  private class Renderer extends GLEventListener {
    /** Called by the drawable to initiate OpenGL rendering by the client.
     * After all GLEventListeners have been notified of a display event, the
     * drawable will swap its buffers if necessary.
     * @param gLDrawable The GLAutoDrawable object.
     */
    private val colors = Array(
      Array(1.0f, 0.5f, 0.5f), Array(1.0f, 0.75f, 0.5f), Array(1.0f, 1.0f, 0.5f),
      Array(0.75f, 1.0f, 0.5f),Array(0.5f, 1.0f, 0.5f), Array(0.5f, 1.0f, 0.75f),
      Array(0.5f, 1.0f, 1.0f), Array(0.5f, 0.75f, 1.0f), Array(0.5f, 0.5f, 1.0f),
      Array(0.75f, 0.5f, 1.0f), Array(1.0f, 0.5f, 1.0f), Array(1.0f, 0.5f, 0.75f)
    )
    private val glu = new GLU
    private val particles = new Array[Particle](1000)
    private val textures = new Array[Int](1)

    private var col, delay = 0
    private var burst, pullLeft, pullRight, pullUp, pullDown = false
    private var rainbow = true
    private var xSpeed, ySpeed: Float = 0
    private var slowDown = 2.0f
    private var z = -40.0f			// Depth Into The Screen


    def display(glDrawable: GLAutoDrawable) {
      def cycleColor { delay = 0; col = (col + 1) % colors.length }

      def update {
        response match {
          case Action.incX => xSpeed = scala.math min(200, xSpeed + 1)
          case Action.decX => xSpeed = scala.math max(-200, xSpeed - 1)
          case Action.incY => ySpeed = scala.math min(200, ySpeed + 1)
          case Action.decY => ySpeed = scala.math max(-200, ySpeed - 1)
          case Action.pullLeft => pullLeft = !pullLeft
          case Action.pullRight => pullRight = !pullRight
          case Action.pullDown => pullDown = !pullDown
          case Action.pullUp => pullUp = !pullUp
          case Action.burst => burst = !burst
          case Action.color => cycleColor
          case Action.rainbow => rainbow = !rainbow
          case Action.slowDown => scala.math min(4, slowDown + 0.01f)
          case Action.speedUp => scala.math.max(1, slowDown - 0.01f)
          case Action.zoomIn => z += 0.1f
          case Action.zoomOut => z -= 0.1f
          case _ =>
        }
        response = Action none

        for(i <- 0 until MAX_PARTICLES)
          if(particles(i).active) {
            // If Number Pad 8 And Y Gravity Is Less Than 1.5 Increase Pull Upwards
            if(pullUp && (particles(i).yg < 1.5f)) particles(i).yg += 0.01f
            // If Number Pad 2 And Y Gravity Is Greater Than -1.5 Increase Pull Downwards
            if(pullDown && (particles(i).yg > -1.5f)) particles(i).yg -= 0.01f
            // If Number Pad 6 And X Gravity Is Less Than 1.5 Increase Pull Right
            if(pullRight && (particles(i).xg < 1.5f)) particles(i).xg += 0.01f
            // If Number Pad 4 And X Gravity Is Greater Than -1.5 Increase Pull Left
            if(pullLeft && (particles(i).xg > -1.5f)) particles(i).xg -= 0.01f

            if(burst) {										
              particles(i).x = 0								// Center On X Axis
              particles(i).y = 0								// Center On Y Axis
              particles(i).z = 0								// Center On Z Axis
              particles(i).xi = (50 * scala.math.random.asInstanceOf[Float] - 26) * 10	// Random Speed On X Axis
              particles(i).yi = (50 * scala.math.random.asInstanceOf[Float] - 25) * 10	// Random Speed On Y Axis
              particles(i).zi = particles(i).yi	// Random Speed On Z Axis
            }
          }
          burst = false
      }

      // display
      val gl = glDrawable.getGL
      gl glClear(GL.GL_COLOR_BUFFER_BIT | GL.GL_DEPTH_BUFFER_BIT)
      gl glLoadIdentity;									// Reset The View
      update

      for(i <- 0 until MAX_PARTICLES)
        if(particles(i).active) {
          val (xp, yp, zp) = (particles(i).x, particles(i).y,
                               particles(i).z + z)

          gl glColor4f(particles(i).r, particles(i).g, particles(i).b,
                       particles(i).life)

          gl glBegin(GL GL_TRIANGLE_STRIP)						// Build Quad From A Triangle Strip
          gl glTexCoord2d(1, 1)
          gl glVertex3f(xp + 0.5f, yp + 0.5f, zp) // Top Right
          gl glTexCoord2d(0, 1)
          gl glVertex3f(xp - 0.5f, yp + 0.5f, zp) // Top Left
          gl glTexCoord2d(1, 0)
          gl glVertex3f(xp + 0.5f, yp - 0.5f, zp) // Bottom Right
          gl glTexCoord2d(0, 0)
          gl glVertex3f(xp - 0.5f, yp - 0.5f, zp) // Bottom Left
          gl glEnd										// Done Building Triangle Strip

          particles(i).x += particles(i).xi / (slowDown * 1000)// Move On The X Axis By X Speed
          particles(i).y += particles(i).yi / (slowDown * 1000)// Move On The Y Axis By Y Speed
          particles(i).z += particles(i).zi / (slowDown * 1000)// Move On The Z Axis By Z Speed

          particles(i).xi += particles(i).xg			// Take Pull On X Axis Into Account
          particles(i).yi += particles(i).yg			// Take Pull On Y Axis Into Account
          particles(i).zi += particles(i).zg			// Take Pull On Z Axis Into Account
          particles(i).life -= particles(i).fade		// Reduce Particles Life By 'Fade'

          if(particles(i).life < 0) {					// If Particle Is Burned Out
            particles(i).life = 1					// Give It New Life
            particles(i).fade = (100 * scala.math.random).asInstanceOf[Float] / 1000 + 0.003f	// Random Fade Value
            particles(i).x = 0						// Center On X Axis
            particles(i).y = 0						// Center On Y Axis
            particles(i).z = 0						// Center On Z Axis
            particles(i).xi = xSpeed + (60 * scala.math.random.asInstanceOf[Float] - 32)	// X Axis Speed And Direction
            particles(i).yi = ySpeed + (60 * scala.math.random.asInstanceOf[Float] - 30)	// Y Axis Speed And Direction
            particles(i).zi = 60 * scala.math.random.asInstanceOf[Float] - 30;	// Z Axis Speed And Direction
            particles(i).r = colors(col)(0)			// Select Red From Color Table
            particles(i).g = colors(col)(1)			// Select Green From Color Table
            particles(i).b = colors(col)(2)			// Select Blue From Color Table
          }
        }
      delay += 1; if(delay > 25 && rainbow) cycleColor
    }

    /** Called when the display mode has been changed.  <B>!! CURRENTLY UNIMPLEMENTED IN JOGL !!</B>
     * @param gLDrawable The GLAutoDrawable object.
     * @param modeChanged Indicates if the video mode has changed.
     * @param deviceChanged Indicates if the video device has changed.
     */
    def displayChanged(glDrawable: GLAutoDrawable, modeChanged: Boolean,
                       deviceChanged: Boolean) {}

    /** Called by the drawable immediately after the OpenGL context is
     * initialized for the first time. Can be used to perform one-time OpenGL
     * initialization such as setup of lights and display lists.
     * @param gLDrawable The GLAutoDrawable object.
     */
    def init(glDrawable: GLAutoDrawable) {
      val gl = glDrawable.getGL

      def loadGLTextures {
        val textImg = TextureReader.readTexture("demos/data/images/particle.png")

        //Create Nearest Filtered Texture
        gl glGenTextures(1, textures, 0)

        gl glBindTexture(GL GL_TEXTURE_2D, textures(0))
        gl glTexParameteri(GL GL_TEXTURE_2D, GL GL_TEXTURE_MAG_FILTER,
                           GL GL_NEAREST)
        gl glTexParameteri(GL GL_TEXTURE_2D, GL GL_TEXTURE_MIN_FILTER,
                           GL GL_NEAREST)
        gl glTexImage2D(GL GL_TEXTURE_2D, 0, 3, textImg.width,textImg.height,
                        0, GL GL_RGB, GL GL_UNSIGNED_BYTE, textImg.pixels)
      }

      // init
      loadGLTextures

      gl glShadeModel(GL GL_SMOOTH)               // Enable Smooth Shading
      gl glClearColor(0, 0, 0, 0)     // Black Background
      gl glClearDepth(1)                       // Depth Buffer Setup
      gl glDisable(GL GL_DEPTH_TEST);                           //Disables Depth Testing
      gl glEnable(GL GL_BLEND);
      gl glBlendFunc(GL GL_SRC_ALPHA, GL GL_ONE);					// Type Of Blending To Perform
      gl glHint(GL GL_PERSPECTIVE_CORRECTION_HINT, GL GL_NICEST) 	
      gl glHint(GL GL_POINT_SMOOTH_HINT, GL GL_NICEST);				// Really Nice Point Smoothing
      gl glEnable(GL GL_TEXTURE_2D)
      gl glBindTexture(GL GL_TEXTURE_2D, textures(0));			// Select Our Texture
      gl glHint(GL GL_POINT_SMOOTH_HINT, GL GL_NICEST);				// Really Nice Point Smoothing

      for(i <- 0 until MAX_PARTICLES) {
        particles(i) = new Particle
        particles(i).active = true								// Make All The Particles Active
        particles(i).life = 1								// Give All The Particles Full Life
        particles(i).fade = 100 * scala.math.random.asInstanceOf[Float] / 1000 + 0.003f	// Random Fade Speed
        particles(i).r = colors(i * 12 / MAX_PARTICLES)(0)	// Select Red Rainbow Color
        particles(i).g = colors(i * 12 / MAX_PARTICLES)(1)	// Select Red Rainbow Color
        particles(i).b = colors(i * 12 / MAX_PARTICLES)(2)	// Select Red Rainbow Color
        particles(i).xi = (50 * scala.math.random.asInstanceOf[Float] - 26) * 10		// Random Speed On X Axis
        particles(i).yi = (50 * scala.math.random.asInstanceOf[Float] - 25) * 10		// Random Speed On Y Axis
        particles(i).zi = particles(i).yi		// Random Speed On Z Axis
        particles(i).xg = 0									// Set Horizontal Pull To Zero
        particles(i).yg = -0.8f								// Set Vertical Pull Downward
        particles(i).zg = 0									// Set Pull On Z Axis To Zero
      }
    }

    /** Called by the drawable during the first repaint after the component has
     * been resized. The client can update the viewport and view volume of the
     * window appropriately, for example by a call to
     * GL.glViewport(int, int, int, int) note that for convenience the component
     * has already called GL.glViewport(int, int, int, int)(x, y, width, height)
     * when this method is called, so the client may not have to do anything in
     * this method.
     * @param gLDrawable The GLAutoDrawable object.
     * @param x The X Coordinate of the viewport rectangle.
     * @param y The Y coordinate of the viewport rectanble.
     * @param width The new width of the window.
     * @param height The new height of the window.
     */
    def reshape(glDrawable: GLAutoDrawable, x: Int, y: Int, width: Int,
                height: Int) {
      require(height != 0)

      val gl = glDrawable.getGL
      val h = width.asInstanceOf[Float] / height.asInstanceOf[Float]

      gl glMatrixMode(GL GL_PROJECTION)
      gl glLoadIdentity;
      glu gluPerspective(45, h, 1, 1000)
      gl glMatrixMode(GL GL_MODELVIEW)
      gl glLoadIdentity
    }
  }
}